home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
FM Towns: Free Software Collection 7
/
FM Towns Free Software Collection 7.iso
/
t_os
/
pigcc
/
pigcc.doc
< prev
next >
Wrap
Text File
|
1993-11-30
|
13KB
|
248 lines
GCC&GASによる円周率の計算 By Jouji
★まえがき
私が、FM TOWNS UX20を買ったのは1992年2月初めでした。そして2
月の末に、迷ったあげくGNU CD rel.2を注文しました。ところが、品不足
という事でなかなか届かず、半分諦めかけていたのですが、5月の末になってようや
く、待望のGNUがやってきました。早速、GCCでお絵描きプログラムを作ったり、
あっちこっちのサンプルプログラムを再コンパイルしたりしてみました。やっぱり、
自分で作ったプログラムがTOWNSで走っているのを見るのは最高の気分ですね。
そのうちに、Oh!FM1990年7月号の若松登志樹氏の記事にも出ていた円周
率の計算をしてみようと思い立ちました。まずは、フリコレ3に入っていた片山慎太
郎氏作のBASICプログラムを見て計算方法を勉強し、ついでに計算をしてみたと
ころ、100桁を28秒で計算できました。そのプログラムを多少自分なりに改良し
てみたら100桁で20秒になりました。さらにCに移植してGCCでコンパイルし
たら、100桁を0.5秒で計算できました。さすがCは速いなと感心し、その時か
ら、どの位まで速くできるかという挑戦が始まりました。
★円周率計算プログラム
最初はCで作っていたのですが、Cでは64ビットの整数が無いため、32ビット
×32ビットおよび64ビット÷32ビットの演算が簡単ではありません。やはり、
このような演算ルーチンはアセンブラで作らなければなりません。386CPUはこ
のようなビット数の演算は1命令で実行できるので、アセンブラで作ればかなりの高
速化が期待できます。GNUのアセンブラはGASですが、GASにはマクロ定義機
能などは無いようです。そこで、GASのソースプログラムをプリプロセッサCPP
に通して、その後アセンブルするようにしました。CPPの置換機能を利用すればG
ASもなかなか使えるものです。具体的には、 make.bat により、
make pigcc.s 2sen exec
のようにコマンドラインから入力すると、CPPにより pigcc.s から pi2sen.s を
作成し、その pi2sen.s に対してアセンブル、リンク、実行(.EXP)ファイルの
作成を行い、続いて実行します。最後の exec パラメータが無ければ実行はしません。
なお、このバッチの実行には環境変数GNUが必要です。 pigcc.s は、もともとは
Cプログラムをコンパイルしてアセンブラソースを出力させたものに手を加えていっ
たものですが、今となってはほとんど原型をとどめていません。main以外は完全
にオリジナルです。ライブラリ関数はprintfとclockを使用しています。
pigcc.s は2000桁計算用になっていますが、NDWの定義値を1行目のコメン
トに従って変更することにより計算桁数を変更できます。コメント文では計算誤差を
見込んで10桁余分に設定しています。また、定義値FDSは、出力制御用のスイッ
チであり、0にすると計算した全桁を出力しますが、1にすると最初の72桁だけ出
力します。これは、計算ルーチンを高速化している過程で使用していたもので、計算
時間だけ知りたい場合に有効です。何しろ1万桁も計算すると画面出力の時間だけで
もかなりのものですから。
プログラムの実行は、T-OS V2.1L10,20のコンソール環境で次のよ
うに行います。
run386 pi2sen.exp
この場合、計算結果を画面に出力します。計算結果は改行も空白も挿入せず、べたに
出力されます。そして、最後に計算時間を出力します。start は計測開始時間、c_en
d は2進数での計算終了時間、d_end は計算結果の出力終了時間です。時間の単位は
1/100秒です。
出力をファイルにリダイレクトする場合は、後で述べる ketaori.exe を使って、
run386 pi2sen.exp | ketaori >pi2sen.dat
というように、桁折りしながら出力した方が良いと思います。
さて、このプログラムのアルゴリズムですが、マチンの公式と呼ばれている次式
(1)を使っています。
π 1 1
― = 4・arctan ― - arctan ――― ‥‥‥(1)
4 5 239
アークタンジェントarctan(x)は、グレゴリー級数と呼ばれている次式(2)の様
なべき級数に展開できます。
1 1
arctan(x)= x - ―x^3 + ―x^5 ・・・
3 5 ‥‥‥(2)
式(1)の arctan を式(2)のべき級数に置き換えることによって、πが計算でき
ます。Oh!FMの若松氏の記事ではシュテルマーの公式を使用していましたが、私
が計算してみた結果ではマチンの公式の方が少し速かったようです。その差は僅かで
したが。
★計算速度について
計算時間は、目標としていた若松氏のものより高速になり、一応、目標達成といっ
たところです。本プログラムと若松氏のプログラムの計算時間の比較を【表1】に示
します。若松氏のプログラムはフリコレ5に入っていたものです。出力は全てRAM
ディスク上のファイルにリダイレクトしています。
【表1】 円周率の計算時間(FM TOWNS)
+----------+----------------+----------------+----------------+
| | 本プログラムを | 若松氏のものを | 若松氏のものを |
| | UX20で実行 | UX20で実行 | モデル2で実行 |
+----------+----------------+----------------+----------------+
| 1万桁 | 52秒 | 1分21秒 | 1分15秒 |
| | (48 + 4) | (53 + 28) | |
+----------+----------------+----------------+----------------+
| 10万桁 | 1時間25分 | 2時間02分 | 1時間52分 |
| | (4776 + 325) | (5310 + 2002) | |
+----------+----------------+----------------+----------------+
【表1】の「モデル2で実行」というのは、若松氏の結果をそのまま載せたもので
す。これを見ると、このような数値計算はやはり386DXが速いのですね。16M
Hzノーウェイトにもかかわらず、386SXでは10%弱遅くなっています。
【表1】の下段のかっこ内の数値は、最初のものが2進数での計算時間、次のもの
が2進→10進変換および出力時間(単位は秒)です。本プログラムでは、2進→1
0進変換および出力時間がかなり短縮されているのが分かると思います。若松氏のプ
ログラムでは円周率の2進計算結果に対して10倍しては整数部分を出力するという
ループになっています。本プログラムでは、2進計算結果に対して10000000
00倍(32ビットで表せる最大の10のべき乗数=10^9)しては整数部分を2
進→10進変換して出力するというループになっています。これによって、円周率の
多桁数値に対するかけ算の回数が1/9になります。
また、本プログラムでは、2進→10進変換して出力するという部分は、実際には
Cライブラリのprintf関数の%d変換を行っているにすぎません。この部分も
自前のルーチンを用意したらさらに高速になるのではないかと思われる方もいるでし
ょう。私もそう思い自前の変換ルーチンをつくり、DOSのファンクションコール9
番によって9文字ずつ出力するようにしてみました。こうすると、画面出力する場合
はpritfよりも高速になりますが、出力をRAMディスク上のファイルにリダイ
レクトした場合はなんとprintfの方が高速であることが分かりました。
この理由ですが、printf関数は多分自前のバッファを持っていて、リダイレ
クトした場合それが有効に機能するのではないかと思います。DOSのシステムコー
ルを呼んだときに生じるrun386.exeのプロテクトモード←→リアルモード
自動変更機能は、便利ではありますがかなりのオーバーヘッドを生ずると考えられま
すから、自前のバッファを持ちシステムコールの回数を減らせばかなりの高速化が期
待できます。printfがそれをやってくれているのならば、簡単だし高速だしの
一石二鳥であり、わざわざ自前の出力ルーチンをつくる必要は無いわけです。
★GASの表記法について
ここで、簡単にGASの表記法について説明します。GASの表記は、MASMな
どとはかなり異なっている点がありますから注意して下さい。いちばん混乱し易いの
は、オペランドの順番がソース、デスティネーションの順であり、MASMなどとは
逆になっている点です。以下に、相違点をまとめます。
●ニーモニックの末尾に、オペランドのデータサイズを示す文字b/w/lを
付ける。b/w/lは、バイト/ワード/ダブルワードを表わす。
●オペランドの順番は、ソース、デスティネーションの順である。
●レジスタの名前には、先頭に%を付ける。
●即値オペランドには、先頭に$を付ける。
例えば、
movl %eax,%ebx
は、32ビットのEAXレジスタからEBXレジスタへの転送を表わします。
★386プログラムの高速化について
では、pigcc.s の中で高速化に留意した点を述べてみましょう。pigcc.s の中で、
計算時間に最も影響を与えるのが、サブルーチン_div_addと_div_su
bです。この2つのサブルーチンはほとんど同じ構造になっていますから、_div
_addによって説明しましょう。_div_addの中でも、
L40:
・
・
loop L40
のループ内ルーチンが実行時間に最も影響するので、この中を重点的に高速化しまし
た。7つの汎用レジスタは全て変数または定数に割り当てて除算2回と加算1回を連
続的に実行しています。工夫した点は、xchg命令を使ってレジスタの内容を交換
し、1ループ内で2回の除算を行うようにしたことです。
また、このループ内の
adcl $0,-4*NDW-8(%esi)
にも注目してください。この行はなくても、同じ動作をするようにプログラムする事
が出来ますが、この行を入れておくことにより条件分岐せずに処理が進む確率が圧倒
的に高くなります。386はプリフェッチ、プリデコードをパイプラインで処理して
いますが、分岐命令があるとプリフェッチ、プリデコードの結果が無効になってしま
い、処理時間が遅くなってしまいます。できるだけ条件分岐や、jmp命令を使わな
いようなプログラムにする事が大切です。
そして、すぐ次の行の
jc L42
ですが、この飛ばし方と飛ばした先からの戻し方もちょっと変ですね。今説明したよ
うに、すぐ上の行のおかげで、この行の条件分岐が成立する確率は非常に小さくなっ
ています。条件分岐命令は、分岐しないときの方が速いですから、このように命令を
配置することによって、条件分岐することなく高速に処理を進めることが出来ます。
また、条件分岐する確率は非常に小さい(約40億分の1)のですから、分岐先のル
ーチンはそれほど高速化に気を使わなくても良いわけです。
★高速化のまとめ
私がこのプログラムを作成し高速化するに当たって気を付けた点を、以下にまとめ
てみます。
(1)データ処理単位を大きくする。8ビットより16ビット、16ビットより
32ビットにする。
(2)複数のループで処理しているものを、1つのループにまとめられないかを
考える。
(3)繰り返し回数の多いループ(内側のループ)を集中的に高速化する。
(4)データのロード,ストア,転送の回数を減らす。
(5)データは可能なかぎりレジスタに保存する。レジスタに対する変数の割り
付けを最適にする。
(6)レジスタに割り付けることが出来ない変数は、高速なアドレッシングモー
ドでアクセスできる格納場所を選ぶ。
(7)分岐を少なくする。条件分岐を入れる場合は、頻度の少ない場合に分岐す
るようにする。
★その他のの注意点
GCCでコンパイルしたアセンブラソースを見ていて気が付いたのですが、GCC
コンパイラは、レジスタeax,ecx,edxをテンポラリな変数として割り当て
るようです。すなわち、これらのレジスタは関数やサブルーチンの内部でその値を保
存する必要がないわけです。それ以外のレジスタは値が保存される通常の変数として
割り当てられるようですから、関数やサブルーチン内で値を変更する場合は、その入
口で保存し、出る前に復帰しておかなければなりません。また、ebpはスタックフ
レームを指すポインタとして使われていますから、あまりいじらない方が良いでしょ
う。
また、これは円周率の計算時間を測定しているときに気が付いたのですが、T-O
SでOAKを組み込んだときと、OAKを外したときでは、計算時間が変化するので
す。最初は原因が分からず、計算時間がころころ変わるので不思議だったのですが、
OAKを組み込んだり外したりしているせいだと分かりました。OAKを組み込むと、
1%程計算時間が長くなります。多分、OAKの割り込み処理か何かが原因だろうと
思います。ということは、OAKを外すとTOWNSが1%速くなるということでし
ょうか。なお、表1の計算時間の測定はOAKを外して行っています。
★桁折りプログラム
リスト1のプログラムは計算した全桁数を改行も何もしないで、べたうちに出力し
てしまいます。私は、エディタはVzを使っていますが、このプログラムで円周率1
0万桁を計算し、その出力ファイルをVzで確かめようとしました。そうしたら、ど
うしたことでしょう、突然Vzがハングアップしてしまいました。考えてみると、1
行が100KByteにもなっているのです。ということは64KByteの壁を越
えており、ハングアップしても無理はないなと思いました。
ということで、長大な行を任意の桁数で桁折りするプログラムも作りました。keta
ori.c がそれです。何の変哲もないフィルタプログラムで、標準入力と標準出力を使
います。パラメータで桁折りする桁数を指定します。パラメータがなければデフォル
トの100桁で桁折りします。私は、LSI C-86試食版でコンパイルしてEX
Eファイル ketaori.exe にしています。次のようにパイプで使用すると良いでしょ
う。
run386 pi10man.exp | ketaori >pi10man.dat
★あとがき
私は、この円周率計算プログラムで10万桁までは計算して確認していますが、1
00万桁の計算は実行していません。このプログラムでは計算時間は桁数の2乗に比
例しますから、100万桁の計算は5.9日もかかります。計算途中経過をセーブで
きるようにして、細切れに計算できるようにすれば良いと思いますが、なかなか面倒
なものですから。
円周率計算プログラムも作り終わった今頃になって、大野栄一氏の「パソコンで挑
む円周率」と金田康正氏の「πのはなし」を読みました。やっぱり円周率って奥が深
いですね。また機会があったら、ガウス・ルジャンドルの公式にも挑戦してみたいと
思います。
最後になりましたが、この記事が、読者諸兄がプログラム開発するときに、少しで
もお役に立てば幸いです。
★参考文献
若松登志樹「YES,I HAVE A NUMBER」Oh!FM1990年7月号
安藤寿茂(elfin)「gasマニュアル」GNU for TOWNS Rel.2
大野栄一「パソコンで挑む円周率」講談社,ブル-バックス
金田康正「πのはなし」東京図書